Skip to content

feat(spec): an Oracle RAW(16) uuid column backs a lookup, master_detail or user field exactly again - #31786

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-h15c0w
Nov 9, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-h15c0w

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #31592

Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: none — git grep -i 'raw(16)|HEXTORAW|isCompatible|type-compat|lossy' -- skills .claude/skills has 0 hits; no skill states this matrix.

维护者速读

  • 改了什么:Oracle 联邦表里 RAW(16) 的 uuid 列,配 lookup / master_detail / user 字段时,类型兼容矩阵从「有损」改回「精确」;配 text 字段仍判有损。只改 packages/spec 的判定函数,外加文档、ADR-0015 §4.6 和 changeset。
  • 为什么改:driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390(PR fix(driver-sql): an Oracle RAW(16) uuid column is filtered and written by the uuid it reads as #31642)已让 driver-sql 用 HEXTORAW(?) 绑定这类列的比较值、写入值和按 id 定位的键,所以 SYS_GUID() 主键上的 lookup 现在能按读出来的 uuid 筛选、写入、改删。矩阵还标着有损,校验就会对每个这样的 lookup 报 type_mismatch 警告。
  • 风险与回滚:这是放宽(Clause-② widening):校验对这三种字段不再报警告。text 不放宽,因为 $contains 这类模式运算符没走 HEXTORAW,Oracle 会拿列的大写无横线十六进制去匹配,用读出来的 uuid 片段搜不到行,也不报错。回滚即 revert 本 PR。
  • 先看:packages/spec/src/data/type-compat.ts(DIALECT_LOSSY_RULES 的 oracle 行和 isCompatible 第一行判断)、docs/adr/0015-external-datasource-federation.md §4.6、content/docs/data-modeling/external-datasources.mdx 的 Oracle 一节。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/type-compat.test.ts,预期 42 个测试全绿;其中 RAW(16) 一条断言 lookup / master_detail / user 为 true、text 为 'lossy'。
  • 需要你做的:审批本 PR(改动含受管面 docs/adr/**,Tier H),之后由席位落地。

What changes

Why

Card task (Ruling-ref: ADR-0015 §4.6): "drop the oracle / uuid demoteExact rule (or narrow it) so RAW(16) → lookup / master_detail answers compatible again". Also: "the pairing must not be declared before the runtime honours it". Triage 6724356074: "Moving a type-compat pair from lossy to compatible widens an acceptance set, which is a metadata-protocol change under #31424 rule 2."

Which paths #31390 binds, read on main (836d408f). I checked these in packages/drivers/driver-sql/src/ and the #31390 tests in sql-driver-oracle-dialect.test.ts.

  • Comparison comparands (=, $eq, $ne, $in, $nin, the range operators) bind through HEXTORAW. See coerceFilterValue and assertOracleUuidComparand, in both the operator-object and bare-equality branches.
  • Written values (create, update, bulk) bind through it.
  • The by-id key of update, delete, bulkDelete and the post-insert read-back binds through it.
  • Lookup expansion reads the target by { id: { $in: [...] } } (packages/objectql/src/engine-summaries-and-expansion.ts L593), which is the comparand path.

So lookup, master_detail and user are honoured, and the narrowing lifts exactly those three.

What is still not honoured: pattern operators. The pin control: a pattern operator on the RAW(16) column compiles its text pattern as before, unjudged shows it: $contains: 'abc' compiles to "ACCT_REF" LIKE ? ESCAPE ? with '%abc%', with no HEXTORAW and no conversion. Oracle converts a RAW to character data as its hex digits (upper case, no hyphens). A fragment of the string a read returns matches no row if it holds a hyphen, and under a case-sensitive operator ($contains, $startsWith) a lower-case fragment matches none at all. No error is raised. For a text field those operators are its own (list search is an $or of $icontains, packages/objectql/src/search-filter.ts), so text stays 'lossy' and a draft keeps its // REVIEW: marker. A reference field is filtered by id equality or membership, which is bound.

Consumers of isCompatible on main (packages/services/service-datasource/src/external-datasource-service.ts):

  • L957, draft // REVIEW:: no change. It judges the suggested type, which for RAW(16) is text, and that stays 'lossy'.
  • L1113, the !== true lookup gate: no change for oracle. It runs only for OData navigationProperties (navigationCandidates), and a SQL table has none.
  • L1339, validateObject: a lookup / master_detail / user field over an oracle RAW(16) column no longer gets the type_mismatch warning. text keeps it.
  • Test pins in service-datasource that hold the 'lossy' answer: none. git grep -n -i "RAW(16)\|demoteExact" in packages/services/service-datasource finds 0. The suite is green unchanged (see Tests).
  • The only other mention is a docblock in driver-sql/src/builtin-column-collision.ts, which is not about oracle.

Tests

  • packages/spec/src/data/type-compat.test.ts, the RAW(16) case: lookup, master_detail, user now pin true, and text pins 'lossy'. It also pins that the demotion promotes nothing (number stays false). The suggested type is still text. Postgres uuid / lookup is still true, and other RAW widths are still binary.
  • Ablation 1 (scripts/ablation-replace.mjs, mutation proven on disk, restored to HEAD): isCompatible back to the flag reading (rule?.demoteExact ? 'lossy' : true). Result: red, lookup: expected 'lossy' to be true, 1 failed / 41 passed.
  • Ablation 2: the oracle row as demoteExact: []. Result: red, expected true to be 'lossy' (the text leg), 1 failed / 41 passed.
  • Both ran against src (the spec suite imports ./type-compat directly), so no dist preflight applies.

Acceptance notes

Verification

Head e090be93 (0a902b02 merged with origin/main at f6474306). main moved nothing under packages/spec or packages/services/service-datasource in that merge.

Suites
  • pnpm --filter @objectstack/spec build: VERDICT command-exit 0, rebuilt after the merge.
  • pnpm --filter @objectstack/spec check:generated: exit 0, "All 14 generated artifacts are up to date".
  • pnpm --filter @objectstack/spec test: 824 files passed, 26081 tests passed, 1 todo, VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0.
  • pnpm --filter @objectstack/service-datasource test (consumer, after its ^... closure build): 74 files, 1299 tests passed, VERDICT command-exit 0.
  • Built dist reading: isCompatible('RAW(16)', X, 'oracle') gives text lossy, lookup true, master_detail true, user true, number false. Postgres uuid / lookup is true, mssql nvarchar(max) / json is lossy.
  • spec test:repo was not run locally. It is CI's (Test Core).
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 108 families. 107 exited 0.
  • pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it needs a full workspace build). That is NOT MEASURED locally, and CI runs it.
  • Three gates first exited 3 on unbuilt prerequisites (check:doc-formula-expressions, check:doc-security-posture, check:skill-examples). Each exited 0 after a locked turbo run build of formula, lint and client-react.
  • After the merge with main, the five gates whose scripts the merge moved were re-run, each exit 0: check:ci-filter-parity, check:cross-package-test-inputs, check:pm-governed-merges, check:engine-double-contract, check:adr-anchors.
  • --ran reconciliation: 108 derived, 107 run, 1 NOT-MEASURED, 0 UNRUN.
  • dispatch-gates --tier: docs/adr/** is Tier H. Changed lines: 94, under 3000.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. pnpm lint is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (check:generated reports none stale.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…ctly again

The SQL driver now binds a RAW(16) comparand, written value and by-id
key through HEXTORAW, so the oracle uuid demotion narrows to the text
pairing alone: lookup, master_detail and user answer exact; text stays
lossy because its pattern operators match the column's hex text.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jULf9M1RFFq4dq9ioH
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e253f3ab09717234fedae8e4d7a193cd1a12b0da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9ae6fc895624857d4ccc8079d9e892adb8b25319 — the merge of head e090be93463b7273a813ee3cdfcb2c7863d6619b into base e253f3ab09717234fedae8e4d7a193cd1a12b0da, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9ae6fc895624857d4ccc8079d9e892adb8b25319 && git checkout 9ae6fc895624857d4ccc8079d9e892adb8b25319
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e253f3ab09717234fedae8e4d7a193cd1a12b0da e090be93463b7273a813ee3cdfcb2c7863d6619b && git checkout -B drift-repro e253f3ab09717234fedae8e4d7a193cd1a12b0da && git merge --no-ff e090be93463b7273a813ee3cdfcb2c7863d6619b

node scripts/docs-audit/affected-docs.mjs --json e253f3ab09717234fedae8e4d7a193cd1a12b0da

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · zhuangjianguo · session_01QsR2jUe9c6YdtNkrzUnMUd

Contract-review-claim: session_01QsR2jUe9c6YdtNkrzUnMUd · head e090be9 · 2026-11-09T04:22Z

The seat runs the CONTRACT_REVIEW_TIER review on an isolated sub-agent (director seat off duty; maintainer rule 2026-11-08 「有 fable 车道就可以契约复审啊」). CI on this head: 57 success, 10 skipped, 0 failed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · zhuangjianguo · session_01QsR2jUe9c6YdtNkrzUnMUd

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e090be93463b7273a813ee3cdfcb2c7863d6619b
Local-runs: none
Scope: packages/spec/src/data/type-compat.ts, docs/adr/0015-external-datasource-federation.md + none

Inputs read: card #31592 (body, triage 6724356074, unblock 6730608741, claim 6731116574, dev report 6731804928), PR #31786 (body, 5-file list, net diff against main at the head), the 67 check-runs on the head (every non-skipped run success: Test Core 1-8, TypeScript Type Check, Lint & Repo Gates, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Build Docs, Build Core; the skipped ones are opt-in or matrix placeholders). The remainder of the diff (type-compat.test.ts, external-datasources.mdx, the changeset) is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only where it pins or restates what the two hits files say.

① Derived judgments

  1. Accept-set widened — isCompatible('RAW(16)', 'lookup' | 'master_detail' | 'user', 'oracle'): 'lossy' → true. Right. Judged against the live driver on main, not the PR body: a comparison comparand on a catalog-reported RAW(16) field binds HEXTORAW(?) in sql-driver-filter-helpers.ts coerceFilterValue L181-188, with the refusal of non-uuid text in the bare-equality branch (L635-637), the operator-object branch (L899-901) and the second equality branch (L1094-1096); a written value binds through formatInput (sql-driver-sqlite-serialisation.ts L263-277, role 'write'); the by-id key binds in update (sql-driver-sequence-semantics.ts L294), delete/bulkDelete (sql-driver-bulk-operations.ts L279-280) and the post-insert read-back (sql-driver-crud.ts L450-454). Lookup expansion reads the target as { id: { $in: ids } } (packages/objectql/src/engine-summaries-and-expansion.ts L593), the comparand path. The oracle pins (sql-driver-oracle-dialect.test.ts L391-470, L473-530) hold each of these shapes. So a reference field over a SYS_GUID() key is filtered, written, keyed and resolved by the string it reads: the pairing the matrix now calls exact is the one the runtime honours.
  2. Accept-set held — isCompatible('RAW(16)', 'text', 'oracle') stays 'lossy'. Right. assertOracleUuidComparand judges only ORACLE_EMPTY_STRING_SENSITIVE_OPERATORS (the comparison set) and coerceFilterValue leaves a non-uuid value alone, so $contains: 'abc' compiles "ACCT_REF" LIKE ? ESCAPE ? against the RAW with no conversion (control pin L434-438). Oracle converts the RAW to its hex text, so a fragment of the read-back string with a hyphen matches no row and raises nothing. A text field's operators are the pattern set (list search is an $or of $icontains, search-filter.ts L206-208), so 'lossy' is the honest answer and the draft's // REVIEW: marker stays (service-datasource L957: the suggested type for RAW(16) is text).
  3. Accept-set unchanged elsewhere. number over RAW(16) stays false (new pin): the demotion list can only move an EXACT pairing to 'lossy', never promote. Other RAW widths stay binary; postgres uuid/lookup stays true; mssql/text addLossy is untouched. DIALECT_LOSSY_RULES is still "only ever toward 'lossy'", as the ADR sentence says.
  4. Public surface. No export changes. DialectLossyRule is module-private (no export on the interface), Compatibility, isCompatible, canonicalizeSqlType and suggestFieldTypeForSqlType keep their signatures. The published change is the matrix's ANSWER, which is why ② is on the metadata-protocol axis.
  5. Consumers on main (packages/services/service-datasource/src/external-datasource-service.ts): validateObject L1339-1356 stops emitting the severity: 'warning' type_mismatch for the three reference types over an oracle RAW(16) column and keeps it for text; the draft path L957 is unchanged (judges the suggested type, text); the !== true lookup gate L1113 lives in draftLookups, which iterates navigationCandidates(table) (OData navigations) — and were a SQL foreign key ever to reach it, drafting a RAW(16) key as a lookup is exactly the widening this card declares. No pin in service-datasource holds the old 'lossy' answer (swept packages/services, packages/objectql, packages/runtime, packages/cli: zero RAW(16) / demoteExact hits), consistent with the green consumer suite in CI.

② Semver level

.changeset/31592-oracle-raw16-reference-exact.md: '@objectstack/spec': minor, carrying Clause-②: yes (widening) and Metadata-protocol: semantic; the PR body carries the same two lines. The diff moves packages/spec/src/** and changes the meaning of a published answer ('lossy' → true for three pairings), which is the semantic kind, and the level rule (scripts/check-changeset-no-major.mjs: added / narrowed / semantic → at least one moved package at minor or above) accepts minor on the moved package. Check Changeset on the head is success. No ! in the title, no (narrowing) arm, no BREAKING banner owed. Right.

Clause-②: yes (widening) — correct: three pairings move from 'lossy' to exact, and nothing is taken away. The changeset's reader-facing text matches the code: the validation warning goes for the three types, text keeps its warning and the draft's review marker, other dialects and other RAW widths are unchanged.

③ Boundary flags

Dev report 6731804928: open_questions: []; four deviations; one out_of_scope_findings entry. The brief flagged one deviation. Each is answered here:

  1. Narrowed, not dropped (demoteExact becomes a FieldType[]; oracle/uuid names text only). Accepted. The card's task line allows "or narrow it", and the narrowing is the only honest shape: dropping the rule would have declared text exact while $contains on the column still matches hex text and not the string a read returns (control pin, sql-driver-oracle-dialect.test.ts L434). The card's "the pairing must not be declared before the runtime honours it" is met for the three types lifted and respected for the one kept.
  2. ADR-0015 §4.6 DIALECT_LOSSY_RULES sentence ("oracle demotes the exact pairings of uuid") also corrected. Accepted. The claim named L431-434 and the Amended by line; the extra sentence is in the same section and the change made it false. An ADR left saying "the exact pairings" over a table that now names one pairing would have been a contract defect, so this is the edit the card owed, not scope creep.
  3. spec test:repo not run locally. Covered by the head's Test Core runs (success).
  4. Live Oracle NOT MEASURED. Accepted with the risk named: the lift rests on the driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390 compiled-SQL pins and on Oracle's documented implicit RAW-to-character conversion, exactly the evidence driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390 itself landed on (its live conformance cell is also unmeasured in CI). This record does not weigh it heavier than driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390 did; it is a known gap of the driver bind, not of the matrix answer, and the matrix answer is the thing under review.
  5. Out-of-scope observation: CANONICAL_TO_FIELD.uuid keeps user exact on every dialect, yet a platform user id is never a uuid. Answered, not escalated as a defect of this PR. The base row's own comment (type-compat.ts L295-306) documents the measurement: better-auth mints a 32-character string and the SQL driver a 16-character nanoid, and the uuid row keeps user because a caller-supplied id is stored verbatim. With this PR oracle joins postgres and mssql in that judgement; the driver bind refuses a non-uuid write with VALIDATION_ERROR and a non-uuid comparand with INVALID_FILTER, so nothing is corrupted — the pairing is honoured at the binding and simply never fits a minted platform id. That is the base row's question on every dialect, the card names lookup / master_detail, and user rides along only because narrowing to text is the honest list. Graded a nit below; it belongs on a follow-up card against the uuid base row, not on this head.

No ruling on the card is contradicted. Triage's lane and grade (domain:spec, p3, metadata-protocol change under #31424 rule 2) match what landed. The claim's file surface (type-compat.ts + test, the mdx rows, ADR §4.6 + Amended by, a changeset, no driver-sql code) is exactly the five files in the diff.

④ Design judgment

packages/spec/src/data/type-compat.ts — judged against ADR-0015 §4.6 (the ruling it implements) and its first consumer (validateObject), and against the driver it describes.

  • Shape. readonly demoteExact?: readonly FieldType[] with rule?.demoteExact?.includes(fieldType) ? 'lossy' : true is the minimal change that keeps the table's invariant (a dialect only ever moves toward 'lossy') and keeps the member live (oracle still uses it). demoteExact: [] demotes nothing, and the dev's two ablations (flag reading restored; empty list) each turn one pin red, so the new pin set discriminates both halves. Nothing is promoted: the list is consulted only inside the entry.exact.includes(fieldType) branch. Holds.
  • Sentence-by-sentence against the driver. "reads a RAW(16) column as a lower-case hyphenated UUID string" — oracleRaw16AsUuid. "binds a comparison comparand, a written value and a by-id key for it as HEXTORAW(?) over that string's hex digits" — oracleUuidRawBinding (HEXTORAW(?) over value.replace(/-/g, '').toUpperCase()), reached from the three sites in ①.1. "A reference field over it … is filtered, written and resolved by the very string it reads" — pins L392-426, L474-530, plus expansion's $in. "its pattern operators … are not bound through HEXTORAW, so they match the column as the text Oracle converts a RAW to (its 32 hex digits, upper case, no hyphens)" — control pin L434-438; assertOracleUuidComparand skips the pattern set by design. Each sentence has code behind it.
  • Tried to break it. $ne / $nin keep their null-safe shape around the bound comparand (pin L416-420); $in with mixed-case members binds every member (L404-408); the range operators are in the comparison set and bind; null stays IS NULL; a master_detail cascade is a deleteMany whose FK comparand binds; create without an id mints a nanoid (sql-driver-crud.ts L287-291) which formatInput then refuses with VALIDATION_ERROR — a refusal, never a silent write; a failed catalog read falls back to binding text (Oracle then errors loudly, the driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390 posture). A VARCHAR2 column holding uuids is still text (the column decides, never the value; pins L428-432, L440-444). None of these breaks the lifted pairings.
  • Finding F1 — nit (prose face). The docblock's closing clause, "a fragment of that string with a hyphen, or in the case it is read in, answers no row instead of a refusal", is overbroad for the folding operators: $icontains / $ilike compile LOWER(col) LIKE LOWER(?) (sql-driver-predicate-emitters.ts textMatchPredicate), so a lower-case, hyphen-free fragment of the read-back string DOES match under them. The test comment and the mdx bullet say it precisely ("matches only when it has no hyphen and the operator ignores case"); the jsdoc should say the same. No effect on the matrix's answer or the contract; fix on the prose face in a later round or leave as an accepted deviation.
  • Finding F2 — nit (follow-up card). user is lifted with the two reference types because text is the only pairing the driver half-serves. The base row's user-under-uuid judgement (③.5) is the pre-existing question on every dialect; with this PR oracle stops warning about it for the wrong reason. Recommend a follow-up card on CANONICAL_TO_FIELD.uuid (does a user field, fixed to sys_user, ever belong in a uuid column's exact set?). Not this card's head.
  • Finding F3 — nit (follow-up or accepted deviation). $search over a lookup named in searchFields: fieldClausesForTerm emits $icontains for reference types (they pass the text-operator door as record-id strings), so a search term that is a fragment of a RAW(16) lookup's id matches only when hyphen-free. This is the same silent-no-row shape the docblock already names for text, on a path no caller uses to resolve a reference (references are filtered by equality or membership, which bind). No effect on the lifted pairings' contract.

docs/adr/0015-external-datasource-federation.md — judged against its first consumer (type-compat.ts + validateObject) and its execution plan.

Implementing files in the diff apart from hits: none. type-compat.ts is both the schema face and the code the ADR sentence describes; the test, the mdx and the changeset are the lane seat's remainder.

Findings: F1 nit, F2 nit, F3 nit. No must-fix, no should-fix: nits alone end the round.

Rendered by the seat's isolated child session 9f9f0eb5-2885-550b-8927-ffc7a5154621 (CLAUDE_CODE_SESSION_ID); the seat below adopts and posts the verdict.

Implemented-by: claude/project-thread-h15c0w
Reviewed-by: session_01QsR2jUe9c6YdtNkrzUnMUd

VERDICT: PASS

Seat adoption note: read before adopting. Spot-checked the pattern-operator premise (coerceFilterValue binds only uuid comparands; a pattern operator's text is never bound through HEXTORAW) and F1's fold path (textMatchPredicate takes a fold flag). Adopted as rendered. F1 to F3 are accepted nits.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · zhuangjianguo · session_01QsR2jUe9c6YdtNkrzUnMUd

维护者速读

席位核对 · ACCEPT · 2026-11-09T04:31Z · head e090be93463b7273a813ee3cdfcb2c7863d6619b

Seat domain:spec#2 reviewed the diff of its own dispatch (claim 6731116574 on #31592, card thread session_01QsR2jULf9M1RFFq4dq9ioH, report 6731804928). Checked against GitHub, not against the report.

  1. Shape: 5 files, +67 / -27. Draft, base main, body opens Fixes #31592. type-compat.ts + test, external-datasources.mdx, ADR-0015 §4.6 + its Amended by line, one changeset (@objectstack/spec minor). Every file is inside the claim. No driver-sql change.
  2. Premise: held at claim time (43fb482) and on this head: oracle: { uuid: { demoteExact: true } } demoted every exact uuid pairing; the driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390 bind (PR fix(driver-sql): an Oracle RAW(16) uuid column is filtered and written by the uuid it reads as #31642, 836d408) is on main and binds comparands, writes and by-id keys, but not pattern-operator text (coerceFilterValue).
  3. Gates / check-runs: on this head 67 runs, 57 success, 10 skipped, 0 red. check-expected-skips --pr 31786: OK (exit 0, roster pinned to main's tip). Dev: dispatch-gates --ran 108 derived, 107 exit 0, 1 NOT MEASURED locally (check:dual-build-cjs-loads, full build; green in CI); spec 824 files and service-datasource 74 files green; two ablations each red.
  4. Accepted deviations: the lift is narrowed, not dropped (demoteExact becomes FieldType[], oracle/uuid keeps text), as the brief allowed ("lift only what is honoured"). The extra ADR-0015 §4.6 sentence on DIALECT_LOSSY_RULES was made false by this change and is corrected in the same section. Live Oracle is not measured, which is the same evidence base as driver-sql (oracle): bind uuid text to RAW(16) through HEXTORAW, so a lookup over a SYS_GUID key filters and writes by its uuid string, and type-compat can lift the pairing from lossy to compatible #31390. The contract review's three nits stand. F1: the jsdoc's "in the case it is read in" is overbroad for the folding operators; the test and the mdx say it precisely. F2: user under the uuid base row is a pre-existing question on every dialect. F3: $search over a RAW(16) lookup id fragment matches only when it is hyphen-free.
  5. Out of scope: text over RAW(16) stays 'lossy' until pattern operators bind; that belongs to the driver, not to this card. Consumers: validateObject drops the warning for the three reference types; the draft's // REVIEW: marker (suggested type text) is unchanged.
  6. Landing: check-governed-merges --pr 31786: governed, Tier H (docs/adr/** ×1). Clause-②: yes (widening), Metadata-protocol: semantic. Contract review at CONTRACT_REVIEW_TIER: PASS, record 6732289043 on this head (an isolated Fable reviewer adopted by this seat, under the maintainer's 2026-11-08 16:12Z rule). The maintainer merges by hand, or an authorized approval (GOVERNED_APPROVERS) and then this seat lands it (ready, auto-merge). On merge: Fixes closes spec(type-compat): lift the oracle RAW(16) → lookup / master_detail pairing from lossy once the driver binds uuid through HEXTORAW #31592; this seat posts the landing note.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 9, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 9, 2026 05:22
@os-zhuang
os-zhuang enabled auto-merge November 9, 2026 05:23
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 75d7fb1 Nov 9, 2026
73 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-h15c0w branch November 9, 2026 05:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue protocol:data size/s tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(type-compat): lift the oracle RAW(16) → lookup / master_detail pairing from lossy once the driver binds uuid through HEXTORAW

3 participants